package rabid;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;

import common.BitManager;
import common.Constants;

/**
 * This class provides an interface to the index
 * 
 * @author Andrew Bernard
 */
public class Index {  
  private BitManager bm;
  private int position = 0;
  private char[] breakCharacters;
  private HashMap customBreakCharacters;
  private File indexFile;
  private Header header;
  private Summary summary;
  private List dataHeaderList;
  private long creationDate;
  
  protected class Summary {
    public int charsInCorpus;
    public int wordsInCorpus;
    public int filesInCorpus;
    
    public Summary(int charsInCorpus, int wordsInCorpus, int filesInCorpus) {
      this.charsInCorpus = charsInCorpus;
      this.wordsInCorpus = wordsInCorpus;
      this.filesInCorpus = filesInCorpus;
    }
  }
  
  protected class Header {
    public String indexPath;
    public String creationDate;
    public String breakChars;
    public String stopFilePath;
    public long indexFileSize;
    
    public Header(String indexPath, String creationDate, String breakChars, String stopFilePath, long indexFileSize) {
      this.indexPath = indexPath;
      this.creationDate = creationDate;
      this.breakChars = breakChars;
      this.stopFilePath = stopFilePath;
      this.indexFileSize = indexFileSize;
    }
  }
  
  protected class DataHeader {
    public String fileName;
    public String shortFileName;
    public int dataStartPos;
    public int charsInFile;
    public int wordsInFile;
    public int filterLen;
    public boolean isOutdated = false;
    /** Creation date of the file this data header is tied to*/
    public long creationDate;
    
    public DataHeader(String fileName, int dataStartPos, int charsInFile, int wordsInFile, int filterLen) {
      this.fileName = fileName;
      this.dataStartPos = dataStartPos;
      this.charsInFile = charsInFile;
      this.wordsInFile = wordsInFile;
      this.filterLen = filterLen;
      
      int index = fileName.lastIndexOf(System.getProperty("file.separator"));
      if(index > 0)
        shortFileName = fileName.substring(index+1);
      else
        shortFileName = fileName;
      
      this.creationDate = new File(fileName).lastModified();
    }
  }
  
  /**
   * Constructor. Loads the index into the bit manager and
   * initializes all the persistent data about the index. All line
   * filters are considered to be volatile and consequently are not
   * stored in memory since it is unknown what the user will search for.
   */
  public Index() {
    try {
      bm = new BitManager(); 
      System.out.print("Initializing index ...");
      bm.load(Constants.INDEX_FILE);
      indexFile = new File(Constants.INDEX_FILE);
      initIndex();
      System.out.println(" done.");
    } 
    catch(IOException ex) {
      System.err.println("The index file could not be loaded.");
      System.exit(-1);
    }
  }
  
  /**
   * Initializes the index by storing all persistent information.
   */
  private void initIndex() {
    // The following must be called in the order shown
    String dateTime = getDateTime();
    String bkChars = getBreakChars();
//    setBreakCharacters(bkChars);
    String stopWordFilePath = getStopWordFilePath();    
    header = new Header(indexFile.getAbsolutePath(), dateTime, bkChars, stopWordFilePath, indexFile.length());
    
    int corpusFileCount = (int)getValue(Constants.NUM_INPUT_FILES_BITS);
    int corpusChars = 0;
    int corpusWords = 0;
//    int corpusLines = 0; 
    
    dataHeaderList = new ArrayList();
    for(int f = 0; f < corpusFileCount; f++) {
      //get the data header info for each file in the index
      String fileName = getInputFileName();
      int charsInFile = (int)getValue(Constants.FILE_CHARS_BITS);
      int wordsInFile = (int)getValue(Constants.FILE_WORDS_BITS);
//      int linesInFile = (int)getValue(Constants.FILE_LINES_BITS);
      int filterLen = (int)getValue(Constants.FILTER_LEN_BITS);
                  
      corpusChars += charsInFile;
      corpusWords += wordsInFile;
//      corpusLines += linesInFile;
      
      DataHeader dataHeader = 
        new DataHeader(fileName, position, charsInFile, wordsInFile, filterLen);
      dataHeaderList.add(dataHeader);
      
//      int dataBodyBits = linesInFile * lineFilterLen;
      skip(filterLen);
    }
    summary = new Summary(corpusChars, corpusWords, corpusFileCount);
  }
    
  /** Returns the size of the index file in bytes */
  public long getFileSize() {
    return indexFile.length();
  }
  
  public List getDataHeaderList() {
    return dataHeaderList;
  }
    
  public Summary getSummary() {
    return summary;
  }
  
  public Header getHeader() {
    return header;
  }
  
  /**
   * Determines if a given word is thought to be in the filter on
   * the specified line
   * @param filter    filter of the line being checked
   * @param lineNum   Line number to check
   * @param checkWord Word to check for
   * @return true if word is 'probably' on line, false otherwise
   */
  public boolean isWordOnLine(int filter, int lineNum, String checkWord) {
    boolean retval = true;
    String word = checkWord.toLowerCase();
    
    int bitOne = Math.abs(word.hashCode() % Constants.DEFAULT_BITS_PER_BITMASK);
    int bitTwo = Math.abs((word.hashCode() * (lineNum + 2)) % Constants.DEFAULT_BITS_PER_BITMASK);
    int bitThree = Math.abs((word.hashCode() / (lineNum + 2)) % Constants.DEFAULT_BITS_PER_BITMASK);

    if ((filter & (1 << bitOne)) != (1 << bitOne)) {
      retval = false;
    }

    if ((filter & (1 << bitTwo)) != (1 << bitTwo)) {
      retval = false;
    }

    if ((filter & (1 << bitThree)) != (1 << bitThree)) {
      retval = false;
    }
    
    return retval;    
  }
  
  /** 
   * Moves the position of index pointer the specified number of bits
   */
  private void skip(int bitsToSkip) {
    position += bitsToSkip;
  }
    
  public void setPosition(int position) {
    this.position = position;
  }
  
  /** 
   * Gets the value from the start of where ever
   * this position pointer is currently at over the span
   * of the number of bits specified
   */
  public long getValue(int numOfBits) {
    long value = bm.getBits(position, numOfBits);
    position += numOfBits;
    return value;
  }
  
  /**
   * Pre: the position is set at the first bit of the break char bitmap
   * as specified by the file format
   * @return a string of all the break characters used in creating the index
   */
  private String getBreakChars() {
    long bits = getValue(64);
    String bitString = getBits(bits, 64);
    bits = getValue(31);
    bitString += Long.toBinaryString(bits);
    char[] breakCharsBitMap = bitString.toCharArray();
    
    String customBreakChars = "";    
    for(int c = 0; c < breakCharsBitMap.length; c++) {
      if(breakCharsBitMap[c] == '1') {
        int breakChar = Constants.FIRST_CUSTOM_BC + c;
        customBreakChars += (char)breakChar;
      }
    }
    return customBreakChars;
  }
  
  /**
   * Pre: position is set to 0 as specified by the file format
   * @return a string representing the index creation date/time
   */
  private String getDateTime() {
    int month = (int)getValue(Constants.MONTH_BITS)-1;
    int day = (int)getValue(Constants.DATE_BITS);
    int year = (int)getValue(Constants.YEAR_BITS);
    int hour = (int)getValue(Constants.HOUR_BITS);
    int min = (int)getValue(Constants.MINUTE_BITS);
    int sec = (int)getValue(Constants.SECOND_BITS);    
    
    GregorianCalendar cal = new GregorianCalendar(year, month, day, hour, min, sec);
    creationDate = cal.getTime().getTime();
    SimpleDateFormat dateText = new SimpleDateFormat("MM/dd/yyyy hh:mm:ss a");
    return dateText.format(cal.getTime());
  }
  
  /**
   * Pre: position is set to the first bit of the stop file name length
   * as specified by the file format
   * @return the absolute path of the stop word file used in creating the index
   */
  private String getStopWordFilePath() {
    int stopFileLen = (int)getValue(Constants.FILE_LENGTH_BITS);
    int length = Constants.FILE_LENGTH_BITS*stopFileLen;
    return getStringFromBits(length);
  }
  
  /**
   * Pre: position is set to the first bit of a data header
   * as specified by the file format
   * @return the name of the indexed file
   */
  private String getInputFileName() {
    int inFileLen = (int)getValue(Constants.FILE_LENGTH_BITS);
    int length = Constants.FILE_LENGTH_BITS*inFileLen;
    return getStringFromBits(length);
  }
  
  /**
   * Utility method.
   * Pre: position is at the first bit of the string to be translated
   * @param length  number of bits in the string being translated 
   * @return a string 
   */
  private String getStringFromBits(int length) {
    String bitString = "";
    while(length > 0) {
      int numOfBits = 0;
      for(; numOfBits < 64 && numOfBits < length; numOfBits++);        
      long bits = getValue(numOfBits);
      bitString += getBits(bits, numOfBits);
      length -= numOfBits;
    }
    
    String string = "";
    for(int i = 0; i < bitString.length(); i+=8) {
      String byteString = bitString.substring(i, i+8);
      int ch = Integer.valueOf(byteString, 2).intValue();      
      string += (char)ch;
    }
    return string;
  }

  /**
   * This won't trim the leading zero's like the .toBinaryString method does
   * @param bits        the bits to translate to a string
   * @param bitsToRead  number of bits to translate to the output string
   * @return bits as a String
   */
  private String getBits(long bits, int bitsToRead) {
    String bitString = "";
    long mask = 1;
    for (int i = 0; i < bitsToRead; i++) {
      long test = bits & mask;
      if (test == mask)
        bitString = "1" + bitString;
      else
        bitString = "0" + bitString;
      mask = mask << 1;
    }
    return bitString;
  }
  
  public long getCreationDate() {
    return creationDate;
  }
}
